package com.ideaaedi.springcloud.jd.commonspring.refresher;

import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.google.common.collect.Sets;
import com.ideaaedi.commonds.time.DateTimeConverter;
import com.ideaaedi.springcloud.jd.commonds.constant.BaseConstant;
import com.ideaaedi.springcloud.jd.commonds.constant.JdSymbolConstant;
import com.ideaaedi.springcloud.jd.commonds.enums.SpecUserTenantEnum;
import com.ideaaedi.springcloud.jd.commonds.enums.user.ApiTypeEnum;
import com.ideaaedi.springcloud.jd.commonds.entity.user.po.SysApiResourcePO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.po.SysMenuApiRePO;
import com.ideaaedi.springcloud.jd.commonspring.config.properties.Knife4jApiInfoProperties;
import com.ideaaedi.springcloud.jd.commonspring.mapper.SysApiResourceMapper;
import com.ideaaedi.springcloud.jd.commonspring.mapper.SysMenuApiReMapper;
import com.ideaaedi.springcloud.jd.commonspring.util.JdContextUtil;
import io.swagger.models.Operation;
import io.swagger.models.Path;
import io.swagger.models.Swagger;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Order;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import springfox.documentation.service.Documentation;
import springfox.documentation.spring.web.DocumentationCache;
import springfox.documentation.swagger2.mappers.ServiceModelToSwagger2Mapper;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 基于源码采集所有request-mapping接口信息, 并刷新至sys_api_resource表
 * <br/>
 * 注：不包括直接通过特殊方式直接注册的request-mapping
 *
 * @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
 * @since 2021.0.1.B
 */
@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE)
public class ApiResourceRefresher implements ApplicationRunner {
    
    public static final String BEAN_NAME = "apiResourceRefresher";
    
    private static final Set<RequestMethod> SUPPORT_REQUEST_METHOD = Sets.newHashSet(RequestMethod.POST, RequestMethod.POST, RequestMethod.POST, RequestMethod.POST);
   
    @Value("${spring.application.name}")
    private String applicationName;

    @Resource
    private DocumentationCache documentationCache;

    @Resource
    private ServiceModelToSwagger2Mapper mapper;

    @Resource
    private SysApiResourceMapper apiResourceMapper;

    @Resource
    private SysMenuApiReMapper sysMenuApiReMapper;
    
    @Resource
    private Knife4jApiInfoProperties knife4jApiInfoProperties;
    
    /**
     * 公开的API前缀
     */
    private final Set<String> PUBLIC_API_SET = Sets.newHashSet("/sys-login/**");
    
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void run(ApplicationArguments args) {
        JdContextUtil.settingUserIdAndTenantIfMiss(SpecUserTenantEnum.SYSTEM_REFRESH_API_RESOURCE);
        try {
            log.info("refresher-api-resource for applicationName -> {} start.", applicationName);
            Documentation documentation = documentationCache.documentationByGroup(applicationName);
            if (documentation == null) {
                documentation = documentationCache.documentationByGroup("default");
                if (documentation == null) {
                    log.info("documentation is null. skip.");
                    return;
                }
            }
            LocalDateTime now = LocalDateTime.now();
            String nowStr = DateTimeConverter.REGULAR_DATE_TIME.convert2Str(now);
            // step1. 筛选出当前服务中的所有api
            List<SysApiResourcePO> newApiInfoList = filterAllApiInCurrService(documentation, now, nowStr);
        
            // step2. 查询数据库中的当前服务所有的api
            List<SysApiResourcePO> oldApiInfoList = apiResourceMapper.selectList(new LambdaQueryWrapper<SysApiResourcePO>()
                    .eq(SysApiResourcePO::getMicroService, applicationName)
            );
            Map<String, SysApiResourcePO> dbPathAndApiResourceMap = oldApiInfoList.stream()
                    .collect(Collectors.toMap(SysApiResourcePO::getPath, Function.identity()));
            log.debug("newApiInfoList -> {}", newApiInfoList);
            log.debug("oldApiInfoList -> {}", oldApiInfoList);
        
            // step3. 筛选出需要刷新的api 并 排序
            List<SysApiResourcePO> toRefreshApiResourceList = filterApiWhoShouldToRefresh(newApiInfoList, dbPathAndApiResourceMap, nowStr);
            toRefreshApiResourceList = sortApi(toRefreshApiResourceList);
        
            // step4. 筛选出需要删除的api
            Set<Long> toRemoveIdSet = filterApiWhoShouldBeDeleted(dbPathAndApiResourceMap, toRefreshApiResourceList);
        
            // step5. 删除多余的api
            if (!CollectionUtils.isEmpty(toRemoveIdSet)) {
                log.debug("to remove api -> {}", JSON.toJSONString(toRemoveIdSet));
                
                List<SysMenuApiRePO> sysMenuApiReList =
                        sysMenuApiReMapper.selectList(new LambdaQueryWrapper<SysMenuApiRePO>()
                        .in(SysMenuApiRePO::getApiId, toRemoveIdSet)
                );
                // 如果api已经被使用了，那么程序这里不能删除， 抛出异常，程序员手动处理数据
                if (!CollectionUtils.isEmpty(sysMenuApiReList)) {
                    throw new IllegalStateException("There is a used api in the apis you are trying to delete. The id of these apis is -> "
                            + sysMenuApiReList.stream().map(SysMenuApiRePO::getApiId).collect(Collectors.toSet()));
                }
                apiResourceMapper.update(new SysApiResourcePO(), new LambdaUpdateWrapper<SysApiResourcePO>()
                                .set(SysApiResourcePO::getDeleted, 1)
                                .set(SysApiResourcePO::getDeletedTime, System.currentTimeMillis() / 1000)
                        .in(SysApiResourcePO::getId, toRemoveIdSet)
                );
            }
        
            // step6. 新增或修改api
            if (!CollectionUtils.isEmpty(toRefreshApiResourceList)) {
                log.debug("to insert or update api -> {}", JSON.toJSONString(toRefreshApiResourceList));
                for (SysApiResourcePO po : toRefreshApiResourceList) {
                    Boolean existCurrApi = po.getExistCurrApi();
                    Assert.notNull(existCurrApi, "existCurrApi should not be null.");
                    if (existCurrApi) {
                        apiResourceMapper.updateById(po);
                    } else {
                        apiResourceMapper.insert(po);
                    }
                }
            }
            log.info("refresher-api-resource for applicationName -> {} end.", applicationName);
        } finally {
            JdContextUtil.clear(SpecUserTenantEnum.SYSTEM_REFRESH_API_RESOURCE);
        }
    }
    
    /**
     * 筛选出当前服务中的所有api
     */
    @NonNull
    private List<SysApiResourcePO> filterAllApiInCurrService(Documentation documentation, LocalDateTime now, String nowStr) {
        Swagger swagger = mapper.mapDocumentation(documentation);
        Map<String, Path> paths = swagger.getPaths();
        List<SysApiResourcePO> newApiInfoList = new ArrayList<>(236);
        paths.forEach((url, info) -> {
            final SysApiResourcePO sysApiResource = new SysApiResourcePO();
            Operation operation = info.getGet();
            if (operation == null) {
                operation = info.getDelete();
            }
            if (operation == null) {
                operation = info.getPost();
            }
            if (operation == null) {
                operation = info.getPut();
            }
            if (operation == null) {
                // api支持的请求方式里，应至少包含这四种中的一种
                throw new IllegalArgumentException(String.format("These methods %s should support at least one at api %s", SUPPORT_REQUEST_METHOD, url));
            }
            
            List<String> tags = operation.getTags().stream().filter(StringUtils::isNotBlank).collect(Collectors.toList());
            if (CollectionUtils.isEmpty(tags)) {
                throw new IllegalArgumentException("Tag should be empty. At api " + url);
            }
            sysApiResource.setName(String.join(", ", tags) + " | " + operation.getSummary());
            sysApiResource.setRemark(JSON.toJSONString(Lists.newArrayList("Created By api-resource-refresher At " + nowStr)));
            String path = handlePathParam(url);
            sysApiResource.setPath(path);
            sysApiResource.setRequestMethod("*");
            sysApiResource.setType(determineType(path));
            sysApiResource.setMicroService(applicationName);
            sysApiResource.setCreatedAt(now);
            newApiInfoList.add(sysApiResource);
        });
        return newApiInfoList;
    }
    
    /**
     * 推断api类型
     */
    @NonNull
    private ApiTypeEnum determineType(String path) {
        for (String ant : PUBLIC_API_SET) {
            if (antPathMatcher.match(ant, path)) {
                return ApiTypeEnum.PUBLIC;
            }
        }
        return ApiTypeEnum.PRIVATE;
    }
    
    /**
     * 筛选出需要刷新的api
     */
    @NonNull
    private static List<SysApiResourcePO> filterApiWhoShouldToRefresh(
            List<SysApiResourcePO> newApiInfoList, Map<String, SysApiResourcePO> dbPathAndApiResourceMap, String nowStr) {
        List<SysApiResourcePO> toRefreshApiResourceList = new ArrayList<>(128);
        for (SysApiResourcePO api : newApiInfoList) {
            if (dbPathAndApiResourceMap.containsKey(api.getPath())) {
                SysApiResourcePO existPo = dbPathAndApiResourceMap.get(api.getPath());
                if (!isSame(api, existPo)) {
                    existPo.setName(api.getName());
                    existPo.setMicroService(api.getMicroService());
                    existPo.setPath(api.getPath());
                    existPo.setType(api.getType());
                    String remark = existPo.getRemark();
                    List<String> remarks = JSON.parseArray(remark, String.class);
                    if (remarks == null) {
                        remarks = Lists.newArrayList();
                    }
                    // 只保留最近remainTotal条变更记录
                    int remainTotal = 10;
                    while (remarks.size() >= remainTotal) {
                        remarks.remove(0);
                    }
                    remarks.add("Updated By api-resource-refresher At " + nowStr);
                    existPo.setRemark(JSON.toJSONString(remarks));
                }
                existPo.setExistCurrApi(true);
                toRefreshApiResourceList.add(existPo);
            }else {
                api.setExistCurrApi(false);
                toRefreshApiResourceList.add(api);
            }
        }
        return toRefreshApiResourceList;
    }
    
    private static boolean isSame(SysApiResourcePO api, SysApiResourcePO existPo) {
        return StringUtils.equals(existPo.getName(), api.getName())
                &&
                StringUtils.equals(existPo.getPath(), api.getPath())
                &&
                StringUtils.equals(existPo.getMicroService(), api.getMicroService())
                &&
                Objects.equals(existPo.getType(), api.getType())
                &&
                StringUtils.equals(existPo.getRequestMethod(), api.getRequestMethod());
    }
    
    /**
     * 筛选出需要删除的api
     */
    @NonNull
    private static Set<Long> filterApiWhoShouldBeDeleted(Map<String, SysApiResourcePO> dbPathAndApiResourceMap,
                                                         List<SysApiResourcePO> toRefreshApiResourceList) {
        Set<String> newPathSet = toRefreshApiResourceList.stream().map(SysApiResourcePO::getPath).collect(Collectors.toSet());
        Set<String> toRemovePathSet = new HashSet<>(dbPathAndApiResourceMap.keySet());
        toRemovePathSet.removeAll(newPathSet);
        Set<Long> toRemoveIdSet= new HashSet<>();
        for (Map.Entry<String, SysApiResourcePO> entry : dbPathAndApiResourceMap.entrySet()) {
            if (toRemovePathSet.contains(entry.getKey())) {
                final Long id = entry.getValue().getId();
                // 内置的跳过，不加入删除集合
                if (id <= BaseConstant.SYS_INTERNAL_DATA_ID_EQ) {
                    continue;
                }
                toRemoveIdSet.add(id);
            }
        }
        return toRemoveIdSet;
    }
    
    /**
     * 排序api
     */
    @NonNull
    private static List<SysApiResourcePO> sortApi(List<SysApiResourcePO> toRefreshApiResourceList) {
        String sysPathPrefix = "sys-";
        toRefreshApiResourceList = toRefreshApiResourceList.stream().sorted(new Comparator<SysApiResourcePO>() {
            /**
             * 排序规则是：
             * 有id：有id的排前面，id小的排前面；
             * 无id：无id的话，比较path，sys-大头的path排前面
             */
            @Override
            public int compare(SysApiResourcePO pre, SysApiResourcePO next) {
                Long preId = pre.getId();
                Long nextId = next.getId();
                if (preId == null && nextId == null) {
                    String prePath = pre.getPath();
                    String nextPath = next.getPath();
                    if (prePath.startsWith(sysPathPrefix) && !nextPath.startsWith(sysPathPrefix)) {
                        return -1;
                    }
                    if (!prePath.startsWith(sysPathPrefix) && nextPath.startsWith(sysPathPrefix)) {
                        return 1;
                    }
                    return prePath.compareTo(nextPath);
                }
                if (preId != null && nextId != null) {
                    return preId.compareTo(nextId);
                }
                // preId为空nextId不为空 或 nextId为空preId不为空
                if (preId == null) {
                    return 1;
                } else {
                    return -1;
                }
            }
        }).peek(po -> {
            if (po.getId() == null) {
                po.setId(IdWorker.getId());
            }
        }).collect(Collectors.toList());
        return toRefreshApiResourceList;
    }
    
    /**
     * 将url中的路径参数替换为**
     */
    public  String handlePathParam(String url) {
        int fromIdx = url.indexOf("{");
        int toIdx = url.indexOf("}", fromIdx + 1);
        while (fromIdx >= 0 && toIdx >= 0) {
            url = url.substring(0, fromIdx) + "**" + url.substring(toIdx + 1);
            fromIdx = url.indexOf("{");
            toIdx = url.indexOf("}", fromIdx);
        }
        // 因为在api文档里面指定了前缀，会在url前多一个前缀，这里把前缀去掉
        String apiPathPrefix = knife4jApiInfoProperties.getApiPathPrefix();
        if (!JdSymbolConstant.SLASH.equals(apiPathPrefix) && url.startsWith(apiPathPrefix)) {
            url = url.substring(apiPathPrefix.length());
        }
        return url;
    }
}
